/**
 *
 */
package co.fitstart.payment.web.controller.wechat;

import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.collections.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.demo2do.core.BusinessException;
import com.demo2do.core.support.Result;
import com.demo2do.core.utils.JsonUtils;
import com.demo2do.core.utils.StringUtils;
import com.demo2do.core.web.utils.WebUtils;
import com.demo2do.core.wechat.pay.UnifiedOrderParameter;
import com.demo2do.core.wechat.pay.WechatQueryParameter;
import com.demo2do.core.wechat.pay.WechatQueryResponse;
import com.demo2do.core.wechat.pay.WechatRefundParameter;
import com.demo2do.core.wechat.pay.handler.WechatpayHandler;

import co.fitstart.payment.Constant;
import co.fitstart.payment.cache.accessor.PersistentAccessor;
import co.fitstart.payment.entity.app.App;
import co.fitstart.payment.entity.app.AppSetting;
import co.fitstart.payment.entity.channel.Channel;
import co.fitstart.payment.entity.request.RefundRecord;
import co.fitstart.payment.entity.request.RequestRecord;
import co.fitstart.payment.entity.transaction.TransactionRecord;
import co.fitstart.payment.entity.transaction.TransactionStatus;
import co.fitstart.payment.service.AppService;
import co.fitstart.payment.service.RefundRecordService;
import co.fitstart.payment.service.RequestRecordService;
import co.fitstart.payment.service.TransactionRecordService;
import co.fitstart.payment.web.handler.RequestValidateHandler;

/**
 * @author lute
 *
 */
@Controller
@RequestMapping("/wx")
public class WechatController {

    private static final Logger logger = LoggerFactory.getLogger(WechatController.class);

    @Value("#{ctx['site.host']}")
    private String host;

    @Autowired
    private PersistentAccessor accessor;

    @Autowired
    private RequestValidateHandler requestValidateHandler;

    @Autowired
    private WechatpayHandler wechatpayHandler;

    @Autowired
    private AppService appService;

    @Autowired
    private RequestRecordService requestRecordService;

    @Autowired
    private TransactionRecordService transactionRecordService;

    @Autowired
    private RefundRecordService refundRecordService;

    /**
     * Prepare checkout payment data and construct WeChat pay necessary data
     *
     * @param appid
     * @param channelName
     * @param orderNo
     * @param customerId
     * @param appNotifyUrl
     * @param signature
     * @param httpServletRequest
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/unifiedorder", method = RequestMethod.POST)
    public String prepay(@RequestParam("appid") String appid,
                         @RequestParam("channel") String channelName,
                         @RequestParam("order_no") String orderNo,
                         @RequestParam("customer_id") String customerId,
                         @RequestParam("app_notify_url") String appNotifyUrl,
                         @RequestParam("signature") String signature,
                         HttpServletRequest httpServletRequest) {

        Result result = new Result();

        // Get app and channel
        App app = appService.getApp(appid);
        Channel channel = accessor.getChannels().get(channelName);
        // Extract post request parameters
        Map<String, String> params = WebUtils.extractPostParameters(httpServletRequest);
        // Validate request parameters
        try {
            requestValidateHandler.validateRequestParameters(app, channel, params);
        } catch (BusinessException e) {
            logger.error("WechatController#prepay - Validate incoming request parameters fails: {}", e);
            return JsonUtils.toJsonString(result.fail().message(e.getMessage()));
        }

        // Create request record
        RequestRecord requestRecord = new RequestRecord(app, channel, orderNo, customerId, appNotifyUrl);
        requestRecordService.create(requestRecord);

        // Get app settings
        Map<String, AppSetting> appSettings = accessor.getAppSettings().get(app.getId());
        String wxAppid = appSettings.get(Constant.SETTING_WX_APPID).getValue();
        String wxMchId = appSettings.get(Constant.SETTING_WX_MCHID).getValue();
        String wxPaySecret = appSettings.get(Constant.SETTING_WX_PAYSCRECT).getValue();

        // Construct unified order parameters
        UnifiedOrderParameter unifiedOrderParameters = constructUnifiedOrderParameters(wxAppid, wxMchId, wxPaySecret, requestRecord.getRequestNo(), params);

        // Request unified order prepay id
        Result prepayResult = wechatpayHandler.requestPrepayId(unifiedOrderParameters);
        if (prepayResult.isValid()) {
            // Request unified order successfully, then create transaction record
            int amount = Integer.valueOf(params.get("total_fee"));
            String prepayId = (String) prepayResult.get("prepayId");
            transactionRecordService.createTransactionRecord(new TransactionRecord(requestRecord, prepayId, amount));
            // Construct success result
            result.success();
            result.setData(prepayResult.getData());
        } else {
            result.fail().message(prepayResult.getMessage());
        }
        return JsonUtils.toJsonString(result);
    }

    /**
     * Construct WeChat unified order parameter
     *
     * @param appid
     * @param mchId
     * @param paySecret
     * @param requestNo
     * @param params
     * @return
     */
    private UnifiedOrderParameter constructUnifiedOrderParameters(String appid, String mchId, String paySecret, String requestNo, Map<String, String> params) {

        int totalFee = Integer.valueOf(params.get("total_fee"));

        UnifiedOrderParameter.Builder builder =
                new UnifiedOrderParameter.Builder(appid, mchId, requestNo, totalFee)
                .attach(params.get("attach"))
                .body(params.get("body"))
                .detail(params.get("detail"))
                .deviceInfo(params.get("device_info"))
                .feeType(params.get("fee_type"))
                .goodsTag(params.get("goods_tag"))
                .notifyUrl(StringUtils.connect(host, "/wx/callback"))
                .openid(params.get("openid"))
                .productId(params.get("product_id"))
                .spbillCreateIp(params.get("spbill_create_ip"))
                .timeExpire(params.get("time_expire"))
                .timeStart(params.get("time_start"))
                .tradeType(params.get("trade_type"));

        // If limit pay parameter exists
        if (!StringUtils.isBlank(params.get("limit_pay"))) {
            builder.limitPay();
        }

        return builder.build(paySecret);
    }

    /**
     * @param appid
     * @param channelName
     * @param orderNo
     * @param totalAmount
     * @param refundAmount
     * @param signature
     * @param httpServletRequest
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/refund", method = RequestMethod.POST)
    public String refund(@RequestParam("appid") String appid,
                         @RequestParam("channel") String channelName,
                         @RequestParam("order_no") String orderNo,
                         @RequestParam("total_amount") int totalAmount,
                         @RequestParam("refund_amount") int refundAmount,
                         @RequestParam("signature") String signature,
                         HttpServletRequest httpServletRequest) {

        Result result = new Result();

        // Get app and channel
        App app = appService.getApp(appid);
        Channel channel = accessor.getChannels().get(channelName);
        // Extract post request parameters
        Map<String, String> params = WebUtils.extractPostParameters(httpServletRequest);
        // Validate request parameters
        try {
            requestValidateHandler.validateRequestParameters(app, channel, params);
        } catch (BusinessException e) {
            logger.error("WechatController#refund - Validate incoming request parameters fails: {}", e);
            return JsonUtils.toJsonString(result.fail().message(e.getMessage()));
        }

        // Get valid paid transaction record according to app, channel and order number
        TransactionRecord transactionRecord = transactionRecordService.getPaidTransactionRecord(app, channel, orderNo);
        if (transactionRecord == null) {
            logger.error("WechatController#refund() - Can not find transaction record with app id [{}], channel id [{}] and order number [{}]", app.getId(), channel.getId(), orderNo);
            return JsonUtils.toJsonString(result.fail().message("Can not find corresponding transaction record"));
        }

        // Check transaction record status
        if (transactionRecord.getTransactionStatus().equals(TransactionStatus.REFUNDED)) {
            logger.error("WechatController#refund() - Order [{}] from app [{}] has been refunded at [{}].", orderNo, appid, transactionRecord.getLastModifiedTime());
            return JsonUtils.toJsonString(result.fail().message("Order has been refunded"));
        }

        // Validate amount equality
        if (transactionRecord.getAmount() != totalAmount) {
            logger.error("WechatController#refund() - Total amount [{}] in refund request is not match with amount [{}] in transaction record.", totalAmount, transactionRecord.getAmount());
            return JsonUtils.toJsonString(result.fail().message("Total amount is not match"));
        }

        // Check whether refund amount is smaller than total amount
        if (refundAmount > totalAmount) {
            logger.error("WechatController#refund() - Refund amount [{}] is bigger than total amount [{}].", refundAmount, totalAmount);
            return JsonUtils.toJsonString(result.fail().message("Refund amount is bigger than total amount"));
        }

        // Check refund amount validity
        if (!isRefundAmountValid(transactionRecord, refundAmount)) {
            logger.error("WechatController#refund() - Refund amount [{}] is too much to larger than amount.", refundAmount);
            return JsonUtils.toJsonString(result.fail().message("Refund amount is too much"));
        }

        // Create refund record
        RefundRecord refundRecord = new RefundRecord(app, channel, transactionRecord, orderNo, totalAmount, refundAmount, null);
        refundRecordService.create(refundRecord);

        // Get necessary app settings from cache
        Map<String, AppSetting> appSettings = accessor.getAppSettings().get(app.getId());
        String wechatAppid = appSettings.get(Constant.SETTING_WX_APPID).getValue();
        String wechatMchId = appSettings.get(Constant.SETTING_WX_MCHID).getValue();
        String wechatPaysecret = appSettings.get(Constant.SETTING_WX_PAYSCRECT).getValue();

        // Construct WeChat refund parameters
        WechatRefundParameter refundParameters = new WechatRefundParameter.Builder(
                                                            wechatAppid,
                                                            wechatMchId,
                                                            transactionRecord.getRequestNo(),
                                                            refundRecord.getRefundNo(),
                                                            totalAmount, refundAmount
                                                        )
                                                        .build(wechatPaysecret);

        // Invoke WechatpayHandler#refund()
        String certPath = StringUtils.connect("/cert/wechat/", app.getAlias(), "/apiclient_cert.p12");
        Result refundResult = wechatpayHandler.refund(refundParameters, certPath);
        if (refundResult.isValid()) {

            String refundNo = (String) refundResult.get("out_refund_no");
            String refundId = (String) refundResult.get("refund_id");
            logger.trace("WechatController#refund() - Get out refund number [{}] and refund id [{}] from WeChat.", refundNo, refundId);

            try {
                refundRecordService.refund(refundNo, refundId);
                result.success().data("refund_no", refundNo);
            } catch (BusinessException be) {
                logger.error("WechatController#refund() - Updating refund record occurs business exception: {}", be);
                result.fail().message(be.getMessage());
            }
        } else {
            logger.error("WechatpayHandlerImpl#refund() - Refunding order [{}] fails. The error message is [{}].", orderNo, refundResult.getMessage());
            result.fail().message("Refunding fails");
        }

        return JsonUtils.toJsonString(result);
    }

    /**
     * Calculate refunded amount total of transaction record
     *
     * @param transactionRecord
     * @param refundAmount
     * @return
     */
    private boolean isRefundAmountValid(TransactionRecord transactionRecord, int refundAmount) {

        // Get all refund records of transaction record
        List<RefundRecord> refundRecords = refundRecordService.getFinishedRefundRecords(transactionRecord);

        int result = 0;
        if (CollectionUtils.isNotEmpty(refundRecords)) {
            for (RefundRecord refundRecord : refundRecords) {
                result += refundRecord.getRefundAmount();
            }
        }

        return refundAmount + result <= transactionRecord.getAmount();
    }

    /**
     * Query WeChat order
     *
     * @param appid
     * @param channelName
     * @param orderNo
     * @param signature
     * @param httpServletRequest
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/orderquery", method = RequestMethod.POST)
    public String queryOrder(@RequestParam("appid") String appid,
                             @RequestParam("channel") String channelName,
                             @RequestParam("order_no") String orderNo,
                             @RequestParam("signature") String signature,
                             HttpServletRequest httpServletRequest) {

        Result result = new Result();

        // Get app and channel
        App app = appService.getApp(appid);
        Channel channel = accessor.getChannels().get(channelName);
        // Extract post request parameters
        Map<String, String> params = WebUtils.extractPostParameters(httpServletRequest);
        // Validate request parameters
        try {
            requestValidateHandler.validateRequestParameters(app, channel, params);
        } catch (BusinessException e) {
            logger.error("WechatController#queryOrder - Validate incoming request parameters fails: {}", e);
            return JsonUtils.toJsonString(result.fail().message(e.getMessage()));
        }

        // Get necessary app settings from cache
        Map<String, AppSetting> appSettings = accessor.getAppSettings().get(app.getId());
        String wechatAppid = appSettings.get(Constant.SETTING_WX_APPID).getValue();
        String wechatMchId = appSettings.get(Constant.SETTING_WX_MCHID).getValue();
        String wechatPaysecret = appSettings.get(Constant.SETTING_WX_PAYSCRECT).getValue();

        // Get all request records according to order number
        List<RequestRecord> requestRecords = requestRecordService.getRequestRecordsOrderedByDescendingTime(orderNo);
        for (RequestRecord requestRecord : requestRecords) {
            if (!requestRecord.isDisabled()) {
                String requestNo = requestRecord.getRequestNo();
                // Build query parameter
                WechatQueryParameter.Builder parametersBuilder = new WechatQueryParameter.Builder(wechatAppid, wechatMchId, requestNo);

                // Get transaction record by request number
                TransactionRecord transactionRecord = transactionRecordService.getTransactionRecord(requestNo);
                if (transactionRecord == null) {
                    logger.warn("WechatController#queryOrder() - Can not find transaction record with request number [{}].", requestNo);
                } else {
                    parametersBuilder.transactionId(transactionRecord.getTradeNo());
                }

                // Go to query result
                WechatQueryResponse response = wechatpayHandler.queryOrder(parametersBuilder.build(wechatPaysecret));
                if (response != null) {
                    if (response.isTradeSuccess()) {
                        // Update transaction record if necessary
                        if (transactionRecord.getTransactionStatus().isCreated()) {
                            String transactionId = response.getTransactionId();
                            transactionRecordService.changeTransactionStatus(transactionRecord, TransactionStatus.PAID, transactionId);
                            // Get transaction record after status update
                            transactionRecord = transactionRecordService.getTransactionRecord(requestNo);
                        }
                        // Return immediately if queried a trade success order
                        result.success().data("appid", transactionRecord.getAppId())
                                        .data("channel_id", transactionRecord.getChannelId())
                                        .data("amount", transactionRecord.getAmount())
                                        .data("order_no", transactionRecord.getOrderNo())
                                        .data("request_no", transactionRecord.getRequestNo())
                                        .data("trade_no", transactionRecord.getTradeNo())
                                        .data("transaction_status", transactionRecord.getTransactionStatus());
                        return JsonUtils.toJsonString(result);
                    } else {
                        String tradeState = response.getTradeState();
                        Long id = transactionRecord.getId();
                        TransactionStatus status = transactionRecord.getTransactionStatus();
                        logger.trace("WechatController#queryOrder - Current transaction record [{}] has status [{}] and recieves trade state [{}]", id, status, tradeState);
                    }
                }
            }
        }
        return JsonUtils.toJsonString(result.fail());
    }

}
